Skip to content

[cxx-interop] Introduce type-level annotations to specify default ownership convention for C++ foreign reference return values #81093

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 4 commits into
base: main
Choose a base branch
from

Conversation

fahadnayyar
Copy link
Contributor

@fahadnayyar fahadnayyar commented Apr 25, 2025

In Swift 6.1, we introduced SWIFT_RETURNS_RETAINED and SWIFT_RETURNS_UNRETAINED annotations for C++ APIs to explicitly specify the ownership convention of SWIFT_SHARED_REFERENCE type return values.

Currently the Swift compiler emits warnings for unannotated C++ APIs returning SWIFT_SHARED_REFERENCE types. We've received some feedback that people are finding these warnings useful to get a reminder to annotate their APIs. While this improves correctness , it also imposes a high annotation burden on adopters — especially in large C++ codebases.

This patch addresses that burden by introducing two new type-level annotations:

  • SWIFT_RETURNS_RETAINED_BY_DEFAULT
  • SWIFT_RETURNS_UNRETAINED_BY_DEFAULT

These annotations allow developers to specify a default ownership convention for all C++ APIs returning a given SWIFT_SHARED_REFERENCE-annotated type, unless explicitly overridden at the API by using SWIFT_RETURNS_RETAINED or SWIFT_RETURNS_UNRETAINED. If a C++ class inherits from a base class annotated with SWIFT_RETURNS_RETAINED_BY_DEFAULT or SWIFT_RETURNS_UNRETAINED_BY_DEFAULT, the derived class automatically inherits the default ownership convention unless it is explicitly overridden. This strikes a balance between safety/correctness and usability:

  • It avoids the need to annotate every API individually.
  • It retains the ability to opt out of the default at the API level when needed.
  • To verify correctness, the user can just remove the SWIFT_RETURNS_(UN)RETAINED_BY_DEFAULT annotation from that type and they will start seeing the warnings on all the unannotated C++ APIs returning that SWIFT_SHARED_REFERENCE type. They can add SWIFT_RETURNS_(UN)RETAINED annotation at each API in which they want a different behaviour than the default. Then they can reintroduce the SWIFT_RETURNS_(UN)RETAINED_BY_DEFAULT at the type level to suppress the warnings on remaining unannotated APIs.

A global default ownership convention (like always return unretained/unowned) was considered but it would weaken the diagnostic signal and remove valuable guardrails that help detect use-after-free bugs and memory leaks in absence of SWIFT_RETURNS_(UN)RETAINED annotations. In the absence of these annotations when Swift emits the unannotated API warning, the current fallback behavior (e.g. relying on heuristics based on API name such as "create", "copy", "get") is derived from Objective-C interop but is ill-suited for C++, which has no consistent naming patterns for ownership semantics.

Several codebases are expected to have project-specific conventions, such as defaulting to unretained except for factory methods and constructors. A type-level default seems like the most precise and scalable mechanism to support such patterns. It integrates cleanly with existing SWIFT_SHARED_REFERENCE usage and provides a per-type opt-in mechanism without global silencing of ownership diagnostics.

This addition improves ergonomics while preserving the safety benefits of explicit annotations and diagnostics.

rdar://145453509

Copy link
Contributor

@Xazax-hun Xazax-hun left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The tests are not passing at the moment.

Also this does not seem to address inheritance. I think it would be really unintuitive that the SHARED_REFERENCE annotations can be inherited but this does not. I suspect that it might not be too much work to support inheritance in this PR so I'd recommend including it in this one rather than doing follow-up work.

@fahadnayyar
Copy link
Contributor Author

Also this does not seem to address inheritance. I think it would be really unintuitive that the SHARED_REFERENCE annotations can be inherited but this does not. I suspect that it might not be too much work to support inheritance in this PR so I'd recommend including it in this one rather than doing follow-up work.

Thanks for bringing this up — I agree that inheritance behavior is an important aspect to consider. That said, I suspect supporting inheritance semantics here could introduce a non-trivial amount of complexity and might make this initial patch harder to review.

The practical downside of not supporting inheritance right away is relatively limited: diagnostics would still be emitted for unannotated APIs returning derived types. In those cases, users can continue to fall back on explicit SWIFT_RETURNS_(UN)RETAINED annotations as needed.

Also, I’m not yet convinced that default ownership annotations should be inherited. One of the core arguments for requiring type-level annotations is that users are already expected to explicitly mark types with SWIFT_SHARED_REFERENCE. They are meant to be a way to suppress diagnostics for a particular C++ foreign reference type as return values of C++ APIs referred from Swift. If we make these annotations also inherited then there might be risk of missing the warnings at some important API.

We've had some internal discussion where opinions have varied on whether ownership defaults should propagate to subclasses, so I think we’d benefit from a broader design conversation before baking inheritance behavior into the model.
Happy to revisit this shortly after landing the base support — especially if we agree on the direction in an offline discussion.

PS: We've added the implicit inheritance of SWIFT_SHARED_REFERENCE very recently and it will ship with Swift 6.2. So till now SWIFT_SHARED_REFERENCE was also not inherited implicitly.

@fahadnayyar fahadnayyar requested a review from DougGregor April 25, 2025 15:56
@Xazax-hun

This comment was marked as resolved.

@Xazax-hun

This comment was marked as resolved.

@egorzhdan

This comment was marked as resolved.

@fahadnayyar fahadnayyar force-pushed the cxx-frt-default-returns-unretained branch from ac02517 to 55cb7cd Compare April 29, 2025 04:57
@fahadnayyar fahadnayyar force-pushed the cxx-frt-default-returns-unretained branch 2 times, most recently from 51e0d54 to 42a2e0e Compare April 29, 2025 06:09
@fahadnayyar fahadnayyar force-pushed the cxx-frt-default-returns-unretained branch from 42a2e0e to f52ef8b Compare April 29, 2025 06:22
@fahadnayyar
Copy link
Contributor Author

@swift-ci please smoke test

@fahadnayyar
Copy link
Contributor Author

@swift-ci please smoke test linux

@fahadnayyar
Copy link
Contributor Author

@swift-ci please smoke test macos

@fahadnayyar
Copy link
Contributor Author

@swift-ci please smoke test macos

@fahadnayyar
Copy link
Contributor Author

This patch now covers inferring SWIFT_RETURNS_RETAINED_BY_DEFAULT and SWIFT_RETURNS_UNRETAINED_BY_DEFAULT annotations from base to derived types in case of C++ inheritance.

@fahadnayyar fahadnayyar requested a review from rjmccall April 30, 2025 00:45
Copy link
Contributor

@j-hui j-hui left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for addressing the concerns about inheritance. It mostly looks good to me and my review primarily consists of nits, but I am concerned about the naming/documentation for this annotation because it is not clear to me what should be annotated with this.

if (const auto *returnPtrTy = retType->getAs<clang::PointerType>()) {
if (clang::RecordDecl *returnRecordDecl =
returnPtrTy->getPointeeType()->getAsRecordDecl()) {
if (auto match = matchSwiftAttrConsideringInheritance<bool>(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: match is unused here, so you don't need if (auto match = condition); just if (condition) should suffice.

Comment on lines +194 to +198
/// Specifies that a C++ API returning this C++ foreign reference type is
/// assumed to return an owned (+1) reference by default, unless explicitly
/// annotated with SWIFT_RETURNS_UNRETAINED. This annotation is only valid on
/// types that are already annotated with SWIFT_SHARED_REFERENCE.
#define SWIFT_RETURNS_RETAINED_BY_DEFAULT \
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry to bike-shed here, but I find the name a bit confusing because SWIFT_RETURNS_RETAINED_BY_DEFAULT is reminiscent of SWIFT_RETURNS_RETAINED, but are used to annotate different language constructs.

I think at the very least, we can update the documentation to first say "this foreign reference type," so that it's clear upfront what is being annotated:

Suggested change
/// Specifies that a C++ API returning this C++ foreign reference type is
/// assumed to return an owned (+1) reference by default, unless explicitly
/// annotated with SWIFT_RETURNS_UNRETAINED. This annotation is only valid on
/// types that are already annotated with SWIFT_SHARED_REFERENCE.
#define SWIFT_RETURNS_RETAINED_BY_DEFAULT \
/// Specifies that this foreign reference type is conventionally returned
/// as an owned (+1) reference. In other words, C++ APIs that return this
/// type which are not explicitly annotated as SWIFT_RETURNS_UNRETAINED
/// are assumed to be implicitly annotated as SWIFT_RETURNS_RETAINED. The
/// SWIFT_RETURNS_RETAINED_BY_DEFAULT annotation is only valid on types
/// that are already annotated with SWIFT_SHARED_REFERENCE.
#define SWIFT_RETURNS_RETAINED_BY_DEFAULT \

An example could be helpful too, but I'm not sure what our appetite for verbosity should be here.

🚲🏠⏬

Returning to my point about why I find the name of this annotation a bit confusing: when we annotate a function f with, say, SWIFT_RETURNS_RETAINED, we are saying "f returns a retained FRT." In other words, the subject of "returns" is f, the function we annotated.

But in this case, when we annotate an FRT T with SWIFT_RETURNS_RETAINED_BY_DEFAULT, we are not saying "T returns a retained FRT by default" (which doesn't really make much sense). Instead, T is the FRT, so we are really saying "T is returned as retained by default convention" (I think "convention" is the slightly more appropriate word choice, though maybe that word is slightly overloaded).

Thus, my suggestion is to rename this annotation to something like SWIFT_RETURNED_AS_RETAINED_BY_CONVENTION or SWIFT_CONVENTIONALLY_RETURNED_AS_RETAINED. That said, I'm not up to speed on existing naming conventions, so I would like others to weigh in here.

@@ -1252,6 +1252,49 @@ enum class ConventionsKind : uint8_t {
CXXMethod = 8,
};

template <typename T>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This helper function should be put into a header file, so that it can be used in both lib/SIL/IR/SILFunctionType.cpp and lib/ClangImporter/ImportDecl.cpp. (Perhaps it can go in ClangImporter.h, in the importer namespace?)


if (const auto *recordDecl = llvm::dyn_cast<clang::CXXRecordDecl>(decl)) {
for (const auto &baseSpecifier : recordDecl->bases()) {
const clang::CXXRecordDecl *baseDecl =
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: you should be able to do const auto *baseDecl here, right? It's already clear what the type of baseDecl is from the name of the method that returns it (i.e., getAsCXXRecordDecl()).


SWIFT_BEGIN_NULLABILITY_ANNOTATIONS

namespace DefaultOwnershipConventionOnCXXForegnRefType {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit:

Suggested change
namespace DefaultOwnershipConventionOnCXXForegnRefType {
namespace DefaultOwnershipConventionOnCXXForeignRefType {

@@ -326,3 +327,175 @@ struct Derived : Base {
FRTStruct *_Nonnull VirtualMethodReturningFRTUnowned() override
__attribute__((swift_attr("returns_unretained")));
};

SWIFT_BEGIN_NULLABILITY_ANNOTATIONS
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is this for? (Not a rhetorical question, I haven't seen this before and do not know what it is)


if (const auto *ptrType =
llvm::dyn_cast<clang::PointerType>(desugaredReturnTy)) {
if (clang::RecordDecl *clangRecordDecl =
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if (clang::RecordDecl *clangRecordDecl =
if (const clang::RecordDecl *clangRecordDecl =

@fahadnayyar
Copy link
Contributor Author

@swift-ci please smoke test macos

if (auto match = matchSwiftAttr<T>(decl, patterns))
return match;

if (const auto *recordDecl = llvm::dyn_cast<clang::CXXRecordDecl>(decl)) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably better to use CXXRecordDecl::forallbases to avoid running out of stack space.

Copy link
Contributor

@egorzhdan egorzhdan left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! I have a couple of comments on the implementation, but the idea looks good to me.

Comment on lines +1421 to +1424
return matchSwiftAttrConsideringInheritance<ResultConvention>(
clangRecordDecl,
{{"returns_unretained_by_default", ResultConvention::Unowned},
{"returns_retained_by_default", ResultConvention::Owned}});
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we move this logic, along with matchSwiftAttr, to ClangImporter? Looks like there is some similar code there already, so I think it's worth refactoring this a bit.

Comment on lines +452 to +453
__attribute__((swift_attr("release:derivedRelease3"))) __attribute__((
swift_attr("returns_unretained_by_default"))) DerivedTypeOverrideDefault
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very minor: this looks mis-formatted

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
c++ interop Feature: Interoperability with C++
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants